﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Storage.EntityModels;
using Storage;

using Lucene.Net.Index;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Search;
using Lucene.Net.QueryParsers;
using Lucene.Net.Highlight;

using RAMDirectory = Lucene.Net.Store.RAMDirectory;
using LDirectory = Lucene.Net.Store.Directory;
using LuceneDictionary = SpellChecker.Net.Search.Spell.LuceneDictionary;

namespace Search
{

    public enum SearchType
    {
        Trip,
        User,
        Document
    }

    /// <summary>
    /// Static class that provides static search methods
    /// </summary>
    public static class SearchManager
    {
        public static List<FullTextResult> FullTextSearch(string query)
        {
            string fake;
            return FullTextSearch(query, 0, 0, CultureInfo.GetCultureInfo("en"), out fake);
        }
        public static List<FullTextResult> FullTextSearch(string query, int from, int offset)
        {
            string fake;
            return FullTextSearch(query, from, offset, CultureInfo.GetCultureInfo("en"), out fake);
        }
        public static List<FullTextResult> FullTextSearch(string query, CultureInfo culture)
        {
            string fake;
            return FullTextSearch(query, 0, 0, culture, out fake);
        }
        public static List<FullTextResult> FullTextSearch(string query, CultureInfo culture, out string sugges)
        {
            return FullTextSearch(query, 0, 0, culture, out sugges);
        }
        
        

        /// <summary>
        /// Provides a full-text search with dividing the result by category.
        /// The signature and the behavior are the same of the FullTextSearch method.
        /// </summary>
        /// <param name="query">The query string</param>
        /// <param name="from">The index of the result the list will begin respect the complete results set</param>
        /// <param name="offset">How many results to retrieve starting from the from parameter</param>
        /// <param name="culture">Identifies the site language to search on</param>
        /// <returns>A list of FullTextCategorizedResult-s </returns>

        public static List<FullTextCategorizedResult> FullTextCategorizedSearch(string query, int from, int offset, CultureInfo culture, out string suggestion)
        {
            List<FullTextResult> results = new List<FullTextResult>();
            results = FullTextSearch(query,from,offset,culture,out suggestion);
            List<FullTextCategorizedResult> categorizedResults=new List<FullTextCategorizedResult>();
            foreach (FullTextResult result in results)
            {
                //set the category by checking the URI
               
                //test solo per la compilazione, finché non ci sarà l'indice vero i risultati
                //della fulltextsearch non sono "reali"
                if (result.Link.ToString().Contains("travel")) 
                {
                     FullTextCategorizedResult categorizedResult = new FullTextCategorizedResult(result.Link,result.PageTitle,result.Snippet,Category.Trip);
                    categorizedResults.Add(categorizedResult);
                }
                
            }
            return categorizedResults;
        }


        /// <summary>
        /// Provides a full-text search with paging feature.
        /// If the offset is equal to 0 the method will return all the results till end.
        /// Lucene.net does not provide pagination methods because they say that doing again the query is
        /// the fastest way due caching.
        /// </summary>
        /// <param name="query">The query string</param>
        /// <param name="from">The index of the result the list will begin respect the complete results set</param>
        /// <param name="offset">How many results to retrieve starting from the from parameter</param>
        /// <param name="culture">Identifies the site language to search on</param>
        /// <returns>A list of FullTextResult-s </returns>
        public static List<FullTextResult> FullTextSearch(string query, int from, int offset, CultureInfo culture, out string suggestion)
        {
            string indexPath = SearchConfig.Instance.IndexFolder;

            
            
            string stopPath = indexPath;
            indexPath += indexPath.EndsWith("\\") ? 
                culture.TwoLetterISOLanguageName : 
                "\\" + culture.TwoLetterISOLanguageName;
            
            if (!IndexReader.IndexExists(indexPath))
            {
                suggestion = "";
                return new List<FullTextResult>();
                //This is only for the (dis)integration day because of the spider.
                //throw new Exception();
            }
            IndexSearcher searcher = new IndexSearcher(indexPath);
            StandardAnalyzer analyzer = new StandardAnalyzer();
            Analyzer stWord;

            // No way switch
            try
            {
                FileInfo IFile = new FileInfo(stopPath + "//" + culture.Name + ".stop");
                stWord = new StopAnalyzer(IFile);
                
            }
            catch (Exception exce)
            {
                Console.WriteLine(exce.Message);
                stWord = new StandardAnalyzer();
                
            }
            suggestion = "";
            //Spellchecker
            if (from == 0)
            {
                LDirectory spellindex = new RAMDirectory();

                SpellChecker.Net.Search.Spell.SpellChecker spellChecker = new SpellChecker.Net.Search.Spell.SpellChecker(spellindex);

                IndexReader r = IndexReader.Open(indexPath);
                spellChecker.IndexDictionary(new SpellChecker.Net.Search.Spell.LuceneDictionary(r, "content"));

                string[] splitString = new string[2];
                splitString[0] = " ";
                splitString[1] = "\"";
                List<String> suggest = new List<string>();
                // Delete multiple white spaces
                string pattern = "\\s+";
                Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
                query = regex.Replace(query, " ");
                string[] sub = query.Split(splitString, StringSplitOptions.None);

                foreach (string j in sub)
                {   
                    
                    String[] similar = spellChecker.SuggestSimilar(j, 3);
                    try
                    {
                        suggest.Add(similar[0]);
                    }
                    catch (IndexOutOfRangeException)
                    {
                        suggest.Add(j);
                    }

                }
                string sugg="";
                foreach (string i in suggest)
                {
                    sugg += i + " ";
                }

                if (sugg == query)
                {
                    suggestion = "";
                }
                else
                {
                    suggestion = sugg;
                }
                
                //end spellchecker
            }



            QueryParser oParser = new QueryParser("content", stWord);
            oParser.SetDefaultOperator(QueryParser.AND_OPERATOR);
            Query searchQuery = oParser.Parse(query);
            Hits oHitColl = searcher.Search(searchQuery);

            Highlighter highlighter = new Highlighter(new QueryScorer(searchQuery));
            highlighter.SetTextFragmenter(new SimpleFragmenter(20));

            //Fill the list with query results.
            Dictionary<string, FullTextResult> pages = new Dictionary<string, FullTextResult>();
            List<FullTextResult> results = new List<FullTextResult>();

            //Added to implement the pagination system. All the errors in the parameters does not 
            //throw any exception.
            if (from <= 0)
            {
                from = 0;
            }
            else
            {
                if (from >= oHitColl.Length())
                {
                    if ((oHitColl.Length() - offset) < 0)
                    {
                        from = 0;
                    }
                    else
                    {
                        from = oHitColl.Length() - offset;
                    }
                }
            }
            int endForResults = from + offset;
            if (endForResults >= oHitColl.Length() || offset <= 0)
            {
                endForResults = oHitColl.Length();
            }
            for (int i = from; i < endForResults; i++)
            {
                Document oDoc = oHitColl.Doc(i);
                String text = oDoc.Get("content");
                int maxNumFragmentsRequired = 3;
                String fragmentSeparator = "...";
                TokenStream tokenStream = analyzer.TokenStream("content", new System.IO.StringReader(text));
                String snippResult = highlighter.GetBestFragments(tokenStream, text, maxNumFragmentsRequired, fragmentSeparator);
                //Console.Out.WriteLine("\t" + snippResult);
                if (!pages.ContainsKey(oDoc.Get("url")))
                {
                    FullTextResult elementResult = new FullTextResult(new Uri(oDoc.Get("url")), oDoc.Get("pagetitle"), snippResult);
                    pages.Add(oDoc.Get("url"), elementResult);
                    results.Add(elementResult);
                }
                
            }
            

            searcher.Close();
            return results;
        }

        /// <summary>
        /// Provides a search over entire trip collection.
        /// </summary>
        /// <param name="start">A TripParameter that indicates the starting point. It can be null</param>
        /// <param name="end">A TripParameter that indicates the ending point. It can be null</param>
        /// <param name="parameters">A list of TripFieldParameter that will be used to build the WHERE clause of the query.
        /// You can specify only trip field that aren't about the User. To perform selection using trip's driver field use
        /// driverParameters instead</param>
        /// <param name="driverParameters">A list of UserParameter that will be used to build the WHERE clause. You can
        /// specify some filters on trip's driver field, perfoming anonymizer checks. </param>
        /// <returns>Trip collection</returns>
        public static IEnumerable<Trip> TripSearch(
            TripParameter start, 
            TripParameter end, 
            List<TripFieldParameter> parameters, 
            List<UserParameter> driverParameters)
        {
            Expression conditions = null;

            ParameterExpression trip = Expression.Parameter(typeof(Trip), "t");
            if (parameters != null && parameters.Count > 0)
            {
                conditions = parameters[0].GetExpression(trip);
                for (int i = 1; i < parameters.Count; i++)
                {
                    conditions = Expression.And(conditions, parameters[i].GetExpression(trip));
                }
            }
            MemberExpression user = Expression.Property(trip, TripFieldParameter.USER_FIELD);
            if (driverParameters != null && driverParameters.Count > 0)
            {
                if (conditions == null) conditions = driverParameters[0].GetExpression(user);
                else conditions = Expression.And(conditions, driverParameters[0].GetExpression(user));

                for (int i = 1; i < parameters.Count; i++)
                {
                    conditions = Expression.And(conditions, driverParameters[i].GetExpression(user));
                }
            }

            Expression<Func<Trip, bool>> predicate = null;
            if (conditions != null) predicate = Expression.Lambda<Func<Trip, bool>>(conditions, trip);

            IQueryable<Trip> trips = null;

            
            if (start == TripParameter.Empty && end == TripParameter.Empty)
                trips = StorageManager.Instance.Entities.Trips;
            else
            {
                float? startLat = null;
                float? startLng = null;
                int? toleranceStart = null;
                float? endLat = null;
                float? endLng = null;
                int? toleranceEnd = null;
                if (start != TripParameter.Empty)
                {
                    startLat = Convert.ToSingle(start.Point.Lat);
                    startLng = Convert.ToSingle(start.Point.Lng);
                    toleranceStart = start.Radius;
                }
                if (end != TripParameter.Empty)
                {
                    endLat = Convert.ToSingle(end.Point.Lat);
                    endLng = Convert.ToSingle(end.Point.Lng);
                    toleranceEnd = end.Radius;
                }
                trips = Storage.StorageManager.Instance.Entities.SearchTrips(
                    startLat, startLng, toleranceStart, endLat, endLng, toleranceEnd, start.ExactMatch, end.ExactMatch);
                    //startLat, startLng, toleranceStart, endLat, endLng, toleranceEnd, true, true);
            }
            
            
            IQueryable<Trip> list;
            if (predicate != null)
                list = trips.Where<Trip>(predicate);
            else
                list = trips;

            return list;
        }

        public static IEnumerable<Trip> TripSearch(
            TripParameter start,
            TripParameter end,
            List<TripFieldParameter> parameters,
            List<UserParameter> driverParameters,
            int startIndex,
            int maxResultSize)
        {
            if (startIndex >= 0 && maxResultSize > 0)
                return TripSearch(start, end, parameters, driverParameters).TakeWhile((t, i) => i >= startIndex && i < startIndex + maxResultSize);
            else 
                return TripSearch(start, end, parameters, driverParameters);
        }

        /// <summary>
        /// Provides a search over entire User collection
        /// </summary>
        /// <param name="parameters">
        /// A list of UserParameter that will be used to build the WHERE clause of the query.
        /// You can specify only fields not covered by Anonymizer. Providing an empty or null 
        /// parameters collection, methos returns entire User collection
        /// </param>
        /// <returns>User collection</returns>
        public static IEnumerable<User> UserSearch(List<UserParameter> parameters)
        {

            ParameterExpression user = Expression.Parameter(typeof(User), "u");

            //initialize parameters with BookedIn filter
            MemberExpression property = Expression.Property(user, "BookedIn");
            Expression<Func<bool, bool>> bookedIn = b => b;
            Expression conditions = Expression.Invoke(bookedIn, property);

            if (parameters != null || parameters.Count > 0)
            {
                for (int i = 0; i < parameters.Count; i++)
                {
                    conditions = Expression.And(conditions, parameters[i].GetExpression(user));
                }
            }


             
            Expression<Func<User,bool>> predicate = Expression.Lambda<Func<User, bool>>(conditions, user);


            IQueryable<User> list = StorageManager.Instance.Entities.Users.Where<User>(predicate);


            return list;
        }

        public static IEnumerable<User> UserSearch(List<UserParameter> parameters, int startIndex, int maxResultSize)
        {
            if (startIndex >= 0 && maxResultSize > 0)
                return UserSearch(parameters).TakeWhile<User>((u, i) => i >= startIndex && i < startIndex + maxResultSize);
            else
                return UserSearch(parameters);
        }

        /// <summary>
        /// This method provides a sql-like search for the users.
        /// The grammar is something like: "FROM" Users WHERE Condition [(AND|OR|NOT) Condition]
        /// Condition ::= Field (like | > | < | <= | >=) Value
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public static IEnumerable<User> SQLUserSearch(string query)
        {
            SearchType retType;
            IEnumerable retList = SQLSearch(query, out retType);
            if (retType == SearchType.User)
            {
                return (IEnumerable<User>)retList;
            }
            else
            {
                throw new Exception("Wrong method usage. User type required.");
            }
        }

        /// <summary>
        /// This method provides a sql-like search for the users.
        /// The grammar is something like: "FROM" Trips WHERE Condition [(AND|OR|NOT) Condition]
        /// Condition ::= Field (like | > | < | <= | >=) Value
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public static IEnumerable<Trip> SQLTripSearch(string query)
        {
            SearchType retType;
            IEnumerable retList = SQLSearch(query, out retType);
            if (retType == SearchType.Trip)
            {
                return (IEnumerable<Trip>)retList;
            }
            else
            {
                throw new Exception("Wrong method usage. Trip type required.");
            }
        }

        /// <summary>
        /// This method provides a generic sql-like search within Users and Trips. You have to give a SearchType
        /// var that the method will set to the specified return type of the results.
        /// </summary>
        /// <param name="query">The query in LOASQL format</param>
        /// <param name="resultType">The type of the result</param>
        /// <returns>List of result of the type resultType</returns>
        public static IEnumerable SQLSearch(string query, out SearchType resultType)
        {
            Stream s = new MemoryStream(ASCIIEncoding.Default.GetBytes(query));
            Search.LOASQL.Scanner scanner = new Search.LOASQL.Scanner(s);
            Search.LOASQL.Parser parser = new Search.LOASQL.Parser(scanner);
            try
            {
                parser.Parse();

                if (parser.ExprType == typeof(User))
                {
                    Expression<Func<User, bool>> pred = Expression.Lambda<Func<User, bool>>(parser.Exp, parser.ExprParam);
                    IQueryable<User> list = StorageManager.Instance.Entities.Users.Where<User>(pred);
                    resultType = SearchType.User;
                    return list;
                }
                else if (parser.ExprType == typeof(Trip))
                {
                    Expression<Func<Trip, bool>> pred = Expression.Lambda<Func<Trip, bool>>(parser.Exp, parser.ExprParam);
                    IQueryable<Trip> list = StorageManager.Instance.Entities.Trips.Where<Trip>(pred);
                    resultType = SearchType.Trip;
                    return list;
                }
                else
                {
                    throw new Exception("Wrong method usage.");
                }
            }
            catch (Exception e)
            {
                throw new Exception(e.Message + "\n" + parser.errors.errMsgFormat);
            }
        }
    }
}
